/*
 * 描          述:  <描述>
 * 修  改   人:  Administrator
 * 修改时间:  2016年10月20日
 * <修改描述:>
 */
package com.tx.core.ddlutil.builder;

import com.tx.core.ddlutil.dialect.Dialect4DDL;
import com.tx.core.ddlutil.model.*;
import com.tx.core.exceptions.util.AssertUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.function.SQLFunction;

import java.io.IOException;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 创建表DDL构建器<br/>
 * <功能详细描述>
 *
 * @author Administrator
 * @version [版本号, 2016年10月20日]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
public abstract class AbstractDDLBuilder<B extends DDLBuilder<B>>
        implements DDLBuilder<B> {

    /**
     * 分行字符.
     */
    private static final String LINE_SEPARATOR = System
            .getProperty("line.separator", "\n");

    /**
     * 表名
     */
    protected String tableName;

    /**
     * 主键字段名
     */
    protected String[] primaryKeyColumnNames;

    /**
     * 字段集合
     */
    protected final List<TableColumnDef> columns = new LinkedList<>();

    /**
     * 索引集合
     */
    protected final List<TableIndexDef> indexes = new LinkedList<>();

    /**
     * 是否要写入字符
     */
    protected StringWriter writer;

    /**
     * <默认构造函数>
     */
    public AbstractDDLBuilder() {
    }

    /**
     * <默认构造函数>
     */
    protected AbstractDDLBuilder(String tableName) {
        this.tableName = tableName;
    }

    /**
     * <默认构造函数>
     */
    protected AbstractDDLBuilder(TableDef table) {
        AssertUtils.notNull(table, "table is null.");
        AssertUtils.notEmpty(table.getName(), "table.tableName is empty.");

        this.tableName = table.getName();
        this.primaryKeyColumnNames = table.getPrimaryKey() == null ? new String[]{} : StringUtils.splitByWholeSeparator(table.getPrimaryKey(), ",");
        this.columns.clear();
        this.columns.addAll(table.getColumns());
        this.indexes.clear();
        this.indexes.addAll(table.getIndexes());
    }

    /**
     * @return 主键字段名集
     */
    public final String getPrimaryKey() {
        if (ArrayUtils.isEmpty(this.primaryKeyColumnNames)) {
            return "";
        }
        List<String> upperCaseColumnNames = this.columns.stream().map(c -> {
            return c.getName().toUpperCase();
        }).collect(Collectors.toList());
        String primaryKeyColumnNames = Arrays.asList(this.primaryKeyColumnNames).stream().filter(pcn -> {
            if (StringUtils.isNotEmpty(pcn) && StringUtils.isNotBlank(pcn) && upperCaseColumnNames.contains(pcn.toUpperCase())) {
                return true;
            } else {
                return false;
            }
        }).map(pcn -> StringUtils.trim(pcn)).collect(Collectors.joining(","));
        return primaryKeyColumnNames;
    }



    /**
     * @param tableColumn
     * @return
     */
    @Override
    public B newColumn(TableColumnDef tableColumn) {
        AssertUtils.notNull(tableColumn, "tableColumn is null.");
        AssertUtils.notEmpty(tableColumn.getName(),
                "tableColumn.columnName is empty.");
        AssertUtils.notNull(tableColumn.getJdbcType(),
                "ddlColumn.jdbcType is empty.");

        addAndValidateNewColumn(tableColumn);

        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }

    /**
     * @param columnName
     * @param size
     * @param required
     * @param defaultValue
     * @return
     */
    @Override
    public B newColumnOfVarchar(String columnName, int size, boolean required,
                                String defaultValue) {
        AssertUtils.notEmpty(columnName, "columnName is empty.");

        JdbcTypeEnum jdbcType = JdbcTypeEnum.VARCHAR;
        if (size <= 0) {
            size = 64;
        } else if (size > 4000 && size < 65535) {
            jdbcType = JdbcTypeEnum.TEXT;
        } else if (size > 65535) {
            jdbcType = JdbcTypeEnum.LONGVARCHAR;
        }

        String defaultValueString = null;
        if (!StringUtils.isEmpty(defaultValue)) {
            defaultValueString = "'" + defaultValue + "'";
        }
        DBColumnDef tableColumn = new DBColumnDef(columnName,
                this.tableName, jdbcType, size, 0, required,
                defaultValueString);

        addAndValidateNewColumn(tableColumn);

        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }

    /**
     * @param columnName
     * @param required
     * @param defaultValue
     * @return
     */
    @Override
    public B newColumnOfInteger(String columnName, boolean required,
                                Integer defaultValue) {
        newColumnOfInteger(columnName, 11, required, defaultValue);

        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }

    /**
     * @param columnName
     * @param size
     * @param required
     * @param defaultValue
     * @return
     */
    @Override
    public B newColumnOfInteger(String columnName, int size, boolean required,
                                Integer defaultValue) {
        AssertUtils.notEmpty(columnName, "columnName is empty.");

        JdbcTypeEnum jdbcType = JdbcTypeEnum.INTEGER;
        if (size <= 0) {
            size = 10;
        } else if (size <= 4) {
            jdbcType = JdbcTypeEnum.TINYINT;
        } else if (size <= 6) {
            jdbcType = JdbcTypeEnum.SMALLINT;
        } else if (size <= 11) {
            jdbcType = JdbcTypeEnum.INT;
        } else if (size < 20) {
            jdbcType = JdbcTypeEnum.BIGINT;
        } else {
            jdbcType = JdbcTypeEnum.NUMERIC;
        }

        String defaultValueString = null;
        if (defaultValue != null) {
            defaultValueString = String.valueOf(defaultValue);
        }
        DBColumnDef tableColumn = new DBColumnDef(columnName,
                this.tableName, jdbcType, size, 0, required,
                defaultValueString);

        addAndValidateNewColumn(tableColumn);

        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }

    /**
     * @param columnName
     * @param required
     * @param isDefaultNow
     * @return
     */
    @Override
    public B newColumnOfDate(String columnName, boolean required,
                             boolean isDefaultNow) {
        AssertUtils.notEmpty(columnName, "columnName is empty.");

        String defaultValue = null;
        if(isDefaultNow){
            Dialect d = getDialect();
            SQLFunction nowSqlFunction = d.getFunctions().get(d.getCurrentTimestampSQLFunctionName());
            if(!nowSqlFunction.hasArguments()){
                defaultValue = nowSqlFunction.render(null, Collections.emptyList(),null);
            }else{
                defaultValue = nowSqlFunction.render(null,Collections.singletonList(6),null);
            }
        }
        //1String defaultCurrentTimestampSQL = d.getCurrentTimestampSQLFunctionName();

        JdbcTypeEnum jdbcType = JdbcTypeEnum.DATETIME;
        DBColumnDef tableColumn = new DBColumnDef(columnName,
                this.tableName, jdbcType, 6, 0, required,
                isDefaultNow ? defaultValue : null);

        addAndValidateNewColumn(tableColumn);

        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }

    /**
     * @param columnName
     * @param required
     * @param defaultValue
     * @return
     */
    @Override
    public B newColumnOfBoolean(String columnName, boolean required,
                                Boolean defaultValue) {
        AssertUtils.notEmpty(columnName, "columnName is empty.");

        JdbcTypeEnum jdbcType = JdbcTypeEnum.BIT;
        TableColumnDef tableColumn = new DBColumnDef(columnName,
                this.tableName, jdbcType, 1, 0, required, defaultValue != null
                ? getDialect().toBooleanValueString(defaultValue.booleanValue()) : null);
                //? (defaultValue.booleanValue() ? getDialect().toBooleanValueString(true) : getDialect().toBooleanValueString(false)) : null);

        addAndValidateNewColumn(tableColumn);

        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }

    /**
     * @param columnName
     * @param size
     * @param scale
     * @param required
     * @param defaultValue
     * @return
     */
    @Override
    public B newColumnOfBigDecimal(String columnName, int size, int scale,
                                   boolean required, BigDecimal defaultValue) {
        AssertUtils.notEmpty(columnName, "columnName is empty.");
        if (size <= 0) {
            size = 16;
        }
        if (scale < 0) {
            scale = 2;
        }
        JdbcTypeEnum jdbcType = JdbcTypeEnum.NUMERIC;
        TableColumnDef tableColumn = new DBColumnDef(columnName,
                this.tableName, jdbcType, size, scale, required,
                defaultValue == null ? null : defaultValue.toString());

        addAndValidateNewColumn(tableColumn);

        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }

    /**
     * 写入一个新字段<br/>
     * <功能详细描述>
     *
     * @param tableColumn [参数说明]
     * @return void [返回类型说明]
     * @throws throws [异常类型] [异常说明]
     * @see [类、类#方法、类#成员]
     */
    private void addAndValidateNewColumn(TableColumnDef tableColumn) {
        AssertUtils.notNull(tableColumn, "ddlColumn is null.");
        AssertUtils.notEmpty(tableColumn.getName(),
                "tableColumn.name is empty.");
        AssertUtils.notNull(tableColumn.getJdbcType(),
                "tableColumn.jdbcType is null.");

        //判断是否有重复的表字段
        for (TableColumnDef tc : this.columns) {
            AssertUtils.notTrue(
                    StringUtils.equalsIgnoreCase(tc.getName(),
                            tableColumn.getName()),
                    "Duplicate column.columnName:{}",
                    tableColumn.getName());
        }

        this.columns.add(tableColumn);//添加字段
    }

    /**
     * @param ddlIndex
     * @return
     */
    @Override
    public B newIndex(TableIndexDef ddlIndex) {
        AssertUtils.notNull(ddlIndex, "ddlIndex is null.");
        AssertUtils.notEmpty(ddlIndex.getName(), "indexName is empty.");
        AssertUtils.notEmpty(ddlIndex.getColumnNames(), "columnNames is empty.");

        addAndValidateNewIndex(ddlIndex);//添加索引

        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }

    /**
     * @param indexName
     * @param columnNames
     * @return
     */
    @Override
    public B newIndex(String indexName, String columnNames) {
        newIndex(false, indexName, columnNames);

        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }

    /**
     * @param unique
     * @param indexName
     * @param columnNames
     * @return
     */
    @Override
    public B newIndex(boolean unique, String indexName, String columnNames) {
        AssertUtils.notEmpty(indexName, "indexName is empty.");
        AssertUtils.notEmpty(columnNames, "columnNames is empty.");

        DBIndexDef newIndex = new DBIndexDef(indexName, columnNames, unique,
                this.tableName);

        addAndValidateNewIndex(newIndex);

        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }

    /**
     * 添加并验证索引<br/>
     * <功能详细描述>
     *
     * @param tableIndex [参数说明]
     * @return void [返回类型说明]
     * @throws throws [异常类型] [异常说明]
     * @see [类、类#方法、类#成员]
     */
    private void addAndValidateNewIndex(TableIndexDef tableIndex) {
        AssertUtils.notNull(tableIndex, "tableIndex is null.");
        AssertUtils.notEmpty(tableIndex.getName(),
                "tableIndex.indexName is empty.");
        AssertUtils.notEmpty(tableIndex.getColumnNames(),
                "tableIndex.columnNames is empty.");

        //判断是否有重复的索引字段
        for (TableIndexDef tidx : this.indexes) {
            //当indexName与columnName都相当时认为是重复的索引
            AssertUtils.notTrue(
                    StringUtils.equalsIgnoreCase(tidx.getName(),
                            tableIndex.getName()),
                    "Duplicate index:indexName:{} columnName:{}",
                    tableIndex.getName(),
                    tidx.getColumnNames());
        }

        this.indexes.add(tableIndex);//添加字段
    }

    /**
     * 写入表注释<br/>
     * <功能详细描述>
     *
     * @param text
     * @return void [返回类型说明]
     * @throws IOException [参数说明]
     * @throws throws      [异常类型] [异常说明]
     * @see [类、类#方法、类#成员]
     */
    protected final void printComment(String text) throws IOException {
        if (this.getDDLDialect().isSqlCommentsOn()) {
            print(this.getDDLDialect().getCommentPrefix());
            print(" ");
            print(text);
            print(" ");
            print(this.getDDLDialect().getCommentSuffix());
            println();
        }
    }

    /**
     * 写入一个字符，和一个换行符
     *
     * @param text The text to print
     */
    protected final void println(String text) throws IOException {
        print(text);
        println();
    }

    /**
     * 写入一个换行符
     */
    protected final void println() throws IOException {
        print(LINE_SEPARATOR);
    }

    /**
     * 写入指定字符集
     *
     * @param text The text to print
     */
    protected final void print(String text) throws IOException {
        this.writer.write(text);
    }

    /**
     * 写入指定字符集
     *
     * @param text The text to print
     */
    protected final void deleteLastIndexOf(String text) throws IOException {
        StringBuffer sb = this.writer.getBuffer();
        if (sb.lastIndexOf(text) < 0) {
            return;
        }
        int start = sb.lastIndexOf(text);
        sb.delete(start, start + text.length());
    }

    /**
     * 设置表名
     * @param tableName
     */
    public final void setTableName(String tableName){
        AssertUtils.notEmpty(tableName, "tableName is empty.");

        this.tableName = tableName;
    }

    /**
     * 设置主键字段名
     *
     * @param columnNames
     * @return
     */
    public B setPrimaryKeyColumnNames(String... columnNames) {
        AssertUtils.notEmpty(columnNames,
                "columnNames is empty.");

        List<String> filterColumnNames = Arrays.asList(columnNames).stream().filter(cn -> {
            if (StringUtils.isNotEmpty(cn) && StringUtils.isNotBlank(cn)) {
                return true;
            } else {
                return false;
            }
        }).collect(Collectors.toList());
        this.primaryKeyColumnNames = filterColumnNames.toArray(new String[filterColumnNames.size()]);
        @SuppressWarnings("unchecked")
        B builder = (B) this;
        return builder;
    }


    /**
     * 获取主键字段集
     *
     * @return
     */
    public final String[] getPrimaryKeyColumnNames() {
        return this.primaryKeyColumnNames;
    }

    /**
     * @return
     */
    public final String tableName() {
        return tableName;
    }



    /**
     * @return 返回 columns
     */
    public List<TableColumnDef> getColumns() {
        return columns;
    }

    /**
     * @return 返回 indexes
     */
    public List<TableIndexDef> getIndexes() {
        return indexes;
    }

    /**
     * @return 返回 ddlDialect
     */
    protected final Dialect4DDL getDDLDialect() {
        return getDatabaseType().getDialect4DDL();
    }

    /**
     * 获取对应的Hibernate方言
     *
     * @return
     */
    protected final Dialect getDialect() {
        return getDatabaseType().getDialect();
    }

    /*public static void main(String[] args) {
        String[] columnNames = new String[]{"1adsf "," 2asdf", "" ," ", "    ", " adsfasdf  "};

        List<String> filterColumnNames = Arrays.asList(columnNames).stream().filter(cn -> {
            if(StringUtils.isNotEmpty(cn) && StringUtils.isNotBlank(cn)){
                return true;
            }else{
                return false;
            }
        }).collect(Collectors.toList());

        String[] res = filterColumnNames.toArray(new String[filterColumnNames.size()]);
        System.out.println(res.length);
        System.out.println(Arrays.asList(res).stream().collect(Collectors.joining(",")));
    }*/

    /*public static void main(String[] args) {
        Dialect d = new MySQL57Dialect();

        System.out.println((new MySQL55Dialect()).getCurrentTimestampSQLFunctionName());
        System.out.println((new MySQLInnoDBDialect()).getCurrentTimestampSQLFunctionName());
        System.out.println(d.getCurrentTimestampSQLFunctionName());
        System.out.println((new MySQL57InnoDBDialect()).getCurrentTimestampSQLFunctionName());
        System.out.println((new MySQL8Dialect()).getCurrentTimestampSQLFunctionName());

        System.out.println(d.getFunctions().get(d.getCurrentTimestampSQLFunctionName()) == null);
        System.out.println(d.getFunctions().get(d.getCurrentTimestampSQLFunctionName()).render(null, Lists.newArrayList(),null));
    }*/
}
